﻿using System;
using System.Reflection.Emit;
using System.Reflection;
using System.Collections.Generic;

namespace net.notdot.XmlOd {
    public interface IStatement {
        void Compile(ILGenerator gen, Scope scope);
    }

    public interface IExpression {
        Type GetEvaluationType(Scope scope);
        void CompileGetter(ILGenerator gen, Scope scope);
    }

    public interface ILiteral : IExpression {
    }

    public interface ILValue : IExpression {
        void CompilePresetter(ILGenerator gen, Scope scope);
        void CompileSetter(ILGenerator gen, Scope scope);
    }

    public partial class ODCode {

        public void Compile(AssemblyBuilder ab, ModuleBuilder modb) {
            Scope codeScope = new Scope();

            foreach (ODClass cls in this.@class)
                cls.Compile(ab, modb, codeScope);
        }
    }

    public partial class ODClass {
        public void Compile(AssemblyBuilder ab, ModuleBuilder modb, Scope codeScope) {
            TypeBuilder tb = modb.DefineType(this.name);
            Scope classScope = new Scope(codeScope);
            foreach (ODMethod meth in this.method) {
                MethodBuilder mb = meth.Compile(tb, classScope);
                if (meth.name == "Main" && meth.@static && meth.GetReturnType(codeScope) == typeof(int) && meth.GetParameterTypes(codeScope).Length == 1 && meth.GetParameterTypes(codeScope)[0] == typeof(string[]))
                    ab.SetEntryPoint(mb);
            }
            tb.CreateType();
        }
    }

    public partial class ODMethod {
        public Type GetReturnType(Scope scope) {
            return scope.ResolveType(this.returns);
        }

        public Type[] GetParameterTypes(Scope scope) {
            return Array.ConvertAll<ODArgument, Type>(this.args, (arg) => scope.ResolveType(arg.type));
        }

        public MethodBuilder Compile(TypeBuilder tb, Scope classScope) {
            Scope methodScope = new Scope(classScope);
            MethodAttributes attribs = MethodAttributes.Public;
            if(this.@static)
                attribs |= MethodAttributes.Static;
            Type[] paramTypes = GetParameterTypes(methodScope);
            MethodBuilder mb = tb.DefineMethod(this.name, attribs, GetReturnType(methodScope), paramTypes);
            for (int i = 0; i < this.args.Length; i++) {
                ParameterBuilder pb = mb.DefineParameter(i, ParameterAttributes.None, this.args[i].name);
                methodScope.AddVariable(this.args[i].name, new ArgumentInfo(pb.Position, paramTypes[i]));
            }

            ILGenerator gen = mb.GetILGenerator();
            foreach (IStatement stat in this.body)
                stat.Compile(gen, methodScope);

            return mb;
        }
    }

    public partial class ODCall : IStatement, IExpression {
        private MethodInfo GetTarget(Scope scope) {
            Type[] types = Array.ConvertAll<object, Type>(this.Items, (o) => (o as IExpression).GetEvaluationType(scope));

            if (this.type != null && this.type != "") {
                //Static method call
                return scope.ResolveType(this.type).GetMethod(this.method, BindingFlags.Static | BindingFlags.Public, null, types, null);
            } else {
                //Instance method call
                Type[] argTypes = new Type[types.Length - 1];
                Array.Copy(types, argTypes, types.Length - 1);
                return types[0].GetMethod(this.method, BindingFlags.Instance | BindingFlags.Public, null, argTypes, null);
            }
        }

        public void Compile(ILGenerator gen, Scope scope) {
            CompileGetter(gen, scope);
            if (GetEvaluationType(scope) != typeof(void))
                gen.Emit(OpCodes.Pop);
        }

        public void CompileGetter(ILGenerator gen, Scope scope) {
            MethodInfo target = GetTarget(scope);
            ParameterInfo[] args = target.GetParameters();
            for(int i = 0; i < this.Items.Length; i++) {
                IExpression item = this.Items[i] as IExpression;
                item.CompileGetter(gen, scope);
                if (item.GetEvaluationType(scope).IsValueType && !args[i].ParameterType.IsValueType)
                    gen.Emit(OpCodes.Box, item.GetEvaluationType(scope));
            }

            gen.EmitCall(OpCodes.Call, target, null);
        }

        public Type GetEvaluationType(Scope scope) {
            return GetTarget(scope).ReturnType;
        }
    }

    public partial class ODWhileLoop : IStatement {
        public void Compile(ILGenerator gen, Scope scope) {
            Label startPos = gen.DefineLabel();
            gen.MarkLabel(startPos);
            Label endPos = gen.DefineLabel();
            (this.condition.Item as IExpression).CompileGetter(gen, scope);
            gen.Emit(OpCodes.Brfalse, endPos);

            Scope childScope = new Scope(scope);
            foreach (IStatement stat in this.body)
                stat.Compile(gen, childScope);

            gen.Emit(OpCodes.Br, startPos);
            gen.MarkLabel(endPos);
        }
    }

    public partial class ODStringLiteral : ILiteral {
        public Type GetEvaluationType(Scope scope) {
            return typeof(string);
        }

        public void CompileGetter(ILGenerator gen, Scope scope) {
            gen.Emit(OpCodes.Ldstr, this.Value);
        }
    }
    
    public partial class ODIntLiteral : ILiteral {
        public Type GetEvaluationType(Scope scope) {
            return typeof(int);
        }

        public void CompileGetter(ILGenerator gen, Scope scope) {
            gen.Emit(OpCodes.Ldc_I4, this.Value);
        }
    }

    public partial class ODReturn : IStatement {
        public void Compile(ILGenerator gen, Scope scope) {
            if(this.Item != null)
                (this.Item as IExpression).CompileGetter(gen, scope);
            gen.Emit(OpCodes.Ret);
        }
    }

    public partial class ODVariableDefinition : IStatement {
        public void Compile(ILGenerator gen, Scope scope) {
            LocalBuilder local = gen.DeclareLocal(scope.ResolveType(this.type));
            local.SetLocalSymInfo(this.name);

            if (this.Item != null) {
                (this.Item as IExpression).CompileGetter(gen, scope);
                gen.Emit(OpCodes.Stloc, local.LocalIndex);
            }

            scope.AddVariable(this.name, local);
        }
    }

    public partial class ODVariableReference : ILValue {
        public Type GetEvaluationType(Scope scope) {
            object var = scope.GetVariable(this.Value);
            if(var.GetType() == typeof(ArgumentInfo))
                return (var as ArgumentInfo).ParameterType;
            if (var.GetType() == typeof(LocalBuilder))
                return (var as LocalBuilder).LocalType;
            return null;
        }

        public void CompileGetter(ILGenerator gen, Scope scope) {
            object var = scope.GetVariable(this.Value);
            if (var.GetType() == typeof(ArgumentInfo)) {
                gen.Emit(OpCodes.Ldarg, (var as ArgumentInfo).Position);
            } else if (var.GetType() == typeof(LocalBuilder)) {
                gen.Emit(OpCodes.Ldloc, var as LocalBuilder);
            } else {
                throw new ArgumentException("Unknown variable type");
            }
        }

        public void CompilePresetter(ILGenerator gen, Scope scope) {
        }

        public void CompileSetter(ILGenerator gen, Scope scope) {
            object var = scope.GetVariable(this.valueField);
            if (var.GetType() == typeof(ArgumentInfo)) {
                gen.Emit(OpCodes.Starg, (var as ArgumentInfo).Position);
            } else if (var.GetType() == typeof(LocalBuilder)) {
                gen.Emit(OpCodes.Stloc, var as LocalBuilder);
            } else {
                throw new ArgumentException("Unknown variable type");
            }
        }
    }

    public partial class ODAssignment : IStatement {
        public void Compile(ILGenerator gen, Scope scope) {
            (this.Item as ILValue).CompilePresetter(gen, scope);
            (this.Item1 as IExpression).CompileGetter(gen, scope);
            (this.Item as ILValue).CompileSetter(gen, scope);
        }
    }

    public abstract class NumberOps {
        private static Type[] typeOrder = { typeof(Int32), typeof(Int64), typeof(Single), typeof(Double) };

        public static Type GetTargetType(Type a, Type b) {
            int aidx = Array.IndexOf<Type>(typeOrder, a);
            int bidx = Array.IndexOf<Type>(typeOrder, b);
            if (aidx < 0 || bidx < 0)
                throw new ArgumentException();
            if (aidx <= bidx)
                return b;
            return a;
        }

        public static void EmitCast(ILGenerator gen, Type a, Type b) {
            if (a == b)
                return;
            if (b == typeof(Int32)) {
                gen.Emit(OpCodes.Conv_I4);
            } else if (b == typeof(Int64)) {
                gen.Emit(OpCodes.Conv_I8);
            } else if (b == typeof(Single)) {
                gen.Emit(OpCodes.Conv_R4);
            } else if (b == typeof(Double)) {
                gen.Emit(OpCodes.Conv_R8);
            }
        }
    }

    public abstract partial class ODArithmeticOperation : IExpression {
        public abstract OpCode GetOpCode();

        public Type GetEvaluationType(Scope scope) {
            Type ret = typeof(int);
            foreach (IExpression expr in this.Items)
                ret = NumberOps.GetTargetType(ret, expr.GetEvaluationType(scope));

            return ret;
        }

        public void CompileGetter(ILGenerator gen, Scope scope) {
            Type target = GetEvaluationType(scope);

            (this.Items[0] as IExpression).CompileGetter(gen, scope);
            NumberOps.EmitCast(gen, (this.Items[0] as IExpression).GetEvaluationType(scope), target);
            for (int i = 1; i < this.Items.Length; i++) {
                (this.Items[i] as IExpression).CompileGetter(gen, scope);
                NumberOps.EmitCast(gen, (this.Items[i] as IExpression).GetEvaluationType(scope), target);
                gen.Emit(GetOpCode());
            }
        }
    }

    public partial class ODAdd : ODArithmeticOperation {
        public override OpCode GetOpCode() {
            return OpCodes.Add;
        }
    }

    public partial class ODSubtract : ODArithmeticOperation {
        public override OpCode GetOpCode() {
            return OpCodes.Sub;
        }
    }

    public abstract partial class ODComparisonOperation : IExpression {
        public abstract OpCode GetOpCode();

        public Type GetEvaluationType(Scope scope) {
            return typeof(bool);
        }

        public void CompileGetter(ILGenerator gen, Scope scope) {
            (this.Items[0] as IExpression).CompileGetter(gen, scope);
            (this.Items[1] as IExpression).CompileGetter(gen, scope);
            gen.Emit(GetOpCode());
        }
    }

    public partial class ODEqual : ODComparisonOperation {
        public override OpCode GetOpCode() {
            return OpCodes.Ceq;
        }
    }

    public partial class ODGreater : ODComparisonOperation {
        public override OpCode GetOpCode() {
            return OpCodes.Cgt;
        }
    }

    public partial class ODLess : ODComparisonOperation {
        public override OpCode GetOpCode() {
            return OpCodes.Clt;
        }
    }

    public partial class ODNew : IExpression {
        public Type GetEvaluationType(Scope scope) {
            return scope.ResolveType(this.type);
        }

        public void CompileGetter(ILGenerator gen, Scope scope) {
            foreach (IExpression expr in this.Items)
                expr.CompileGetter(gen, scope);
            Type target = scope.ResolveType(this.type);
            Type[] argTypes = Array.ConvertAll<object, Type>(this.Items, (o)=>(o as IExpression).GetEvaluationType(scope));
            ConstructorInfo ctor = target.GetConstructor(argTypes);
            gen.Emit(OpCodes.Newobj, ctor);
        }
    }

    public partial class ODIndex : ILValue {
        public void CompilePresetter(ILGenerator gen, Scope scope) {
            if (!(this.Items[0] is ILValue))
                throw new InvalidOperationException();

            foreach (IExpression expr in this.Items)
                expr.CompileGetter(gen, scope);
        }

        public void CompileSetter(ILGenerator gen, Scope scope) {
            Type arrayType = (this.Items[0] as IExpression).GetEvaluationType(scope);
            if (arrayType.IsArray) {
                if (arrayType.GetArrayRank() == 1) {
                    gen.Emit(OpCodes.Stelem, arrayType.GetElementType());
                } else {
                    gen.EmitCall(OpCodes.Call, arrayType.GetProperty("get").GetGetMethod(), null);
                }
            } else {
                gen.EmitCall(OpCodes.Call, arrayType.GetProperty("item").GetGetMethod(), null);
            }
        }

        public Type GetEvaluationType(Scope scope) {
            Type arrayType = (this.Items[0] as IExpression).GetEvaluationType(scope);

            if (arrayType.IsArray) {
                return arrayType.GetElementType();
            } else {
                return arrayType.GetProperty("this").PropertyType;
            }
        }

        public void CompileGetter(ILGenerator gen, Scope scope) {
            foreach (IExpression expr in this.Items)
                expr.CompileGetter(gen, scope);

            Type arrayType = (this.Items[0] as IExpression).GetEvaluationType(scope);
            if (arrayType.IsArray) {
                if (arrayType.GetArrayRank() == 1) {
                    gen.Emit(OpCodes.Ldelem, arrayType.GetElementType());
                } else {
                    gen.EmitCall(OpCodes.Call, arrayType.GetProperty("get").GetGetMethod(), null);
                }
            } else {
                gen.EmitCall(OpCodes.Call, arrayType.GetProperty("this").GetGetMethod(), null);
            }
        }
    }

    public partial class ODGet : ILValue {
        public void CompilePresetter(ILGenerator gen, Scope scope) {
            (this.Item as IExpression).CompileGetter(gen, scope);
        }

        public void CompileSetter(ILGenerator gen, Scope scope) {
            Type elementType = (this.itemField as IExpression).GetEvaluationType(scope);
            PropertyInfo prop = elementType.GetProperty(this.property);
            gen.EmitCall(OpCodes.Call, prop.GetSetMethod(), null);
        }

        public Type GetEvaluationType(Scope scope) {
            Type elementType = (this.Item as IExpression).GetEvaluationType(scope);
            return elementType.GetProperty(this.property).PropertyType;
        }

        public void CompileGetter(ILGenerator gen, Scope scope) {
            Type elementType;
            if (this.type != null) {
                elementType = scope.ResolveType(this.type);
            } else {
                elementType = (this.Item as IExpression).GetEvaluationType(scope);
                (this.Item as IExpression).CompileGetter(gen, scope);
            }

            PropertyInfo prop = elementType.GetProperty(this.property);
            gen.EmitCall(OpCodes.Call, prop.GetGetMethod(), null);
        }
    }

    public partial class ODEnumValue : IExpression {
        public Type GetEvaluationType(Scope scope) {
            return Enum.GetUnderlyingType(scope.ResolveType(this.type));
        }

        public void CompileGetter(ILGenerator gen, Scope scope) {
            Type enumType = GetEvaluationType(scope);
            if (enumType == typeof(Int64) || enumType == typeof(UInt64)) {
                Int64 val = Convert.ToInt64(Enum.Parse(enumType, this.value));
                gen.Emit(OpCodes.Ldc_I8, val);
            } else {
                Int32 val = Convert.ToInt32(Enum.Parse(enumType, this.value));
                gen.Emit(OpCodes.Ldc_I4, val);
            }
        }
    }
}